<?php

/*
 * Copyright (C) 2009 - 2011 Pham Cong Dinh
 *
 * This file is part of Spica.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 3 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

// namespace \spica\core\net;

include_once 'library/spica/core/net/HttpClient.php';

/**
 * AsyncHttpClient is a client which allow applications to execute HTTP requests and asynchronously
 * process the HTTP response.
 *
 * namespace \spica\core\net\AsyncHttpClient;
 *
 * @category   spica
 * @package    core
 * @subpackage net
 * @author     Pham Cong Dinh <pcdinh at phpvietnam dot net>
 * @since      Version 0.3
 * @since      February 18, 2010
 * @copyright  Pham Cong Dinh (http://www.phpvietnam.net)
 * @license    http://www.gnu.org/licenses/lgpl-3.0.txt
 * @version    $Id: AsyncHttpClient.php 1869 2011-01-07 18:55:25Z pcdinh $
 */
class SpicaAsyncHttpClient
{
    /**
     * Request counter.
     *
     * @var int
     */
    private $_counter = 0;

    /**
     * Separated requests.
     *
     * @var array
     */
    private $_requests = array();

    /**
     * Session handle.
     *
     * @var resource
     */
    private $_handle;

    /**
     * Curl executor.
     *
     * @var SpicaCurlExecutor
     */
    private $_executor;

    /**
     * Timeout (in seconds) for complete download process including all file transfer (default 5 minutes)
     *
     * @var int
     */
    public $timeout = 300;

    /**
     * Timeout (in seconds) for connection to server; this is the timeout that
     * usually happens if the remote server is completely down (default 20 seconds);
     * may not work when using proxy
     *
     * @var int
     */
    public $connectTimeout = 20;

    /**
     * Is return content required?
     *
     * @var int
     */
    public $contentRequired = 1;
    /**
     * Is return header required?
     *
     * @var int
     */
    public $headerRequired = 0;

    /**
     * HTTP proxy.
     *
     * @var SpicaHttpProxy
     */
    protected $_proxy;

    /**
     * Request IP.
     *
     * @var string
     */
    protected $_ip;

    /**
     * Is session closed?
     *
     * @var bool
     */
    protected $_isClosed = false;

    /**
     * Constructs an object of <code>SpicaAsyncHttpClient</code>
     *
     * @param SpicaCurlExecutor $executor
     */
    public function __construct($executor = null)
    {
        $this->_executor = $executor;
    }

    /**
     * Sets proxy.
     *
     * @param SpicaHttpProxy $proxy
     */
    public function setProxy($proxy)
    {
        $this->_proxy = $proxy;
    }

    /**
     * Sets IP.
     *
     * The interface name, ip address, or host name must belong to the local machine.
     *
     * This settings will affect $_SERVER['REMOTE_ADDRESS'] in server.
     *
     * @param string $ip
     */
    public function setInterface($ip)
    {
        $this->_ip = $ip;
    }

    /**
     * Gets local machine's interface name, IP or host name.
     *
     * @return string
     */
    public function getInterface()
    {
        return $this->_ip;
    }

    /**
     * Adds a request.
     * 
     * @param  SpicaHttpRequest $request
     * @param  string $name
     * @return SpicaAsyncResponseManager
     */
    public function add($request, $name = null)
    {
        $key = $this->_counter++;

        if (null === $name)
        {
            $this->_requests[$key] = $request;
        }
        else
        {
            $this->_requests[$name] = $request;
        }
    }

    /**
     * Sends HTTP requests based on the current configuration.
     *
     * @throws Exception when an error occurs
     * @param  SpicaHttpRequest $request the Http request
     * @return SpicaFutureResponseHandler
     */
    public function sendRequest()
    {
        if (null === $this->_executor)
        {
            $this->_executor = new SpicaCurlExecutor();
        }

        $this->_handle = curl_multi_init();
        $names = array();
        $opts = array();

        foreach ($this->_requests as $name => $request)
        {
            /* @var $request SpicaHttpRequest */
            $names[$name] = curl_init();
            $this->_executor->prepare($request, $names[$name], $this->timeout, $this->connectTimeout, $this->_ip);
            curl_multi_add_handle($this->_handle, $names[$name]);
            $opts[(string) $names[$name]] = array(
                'max_redirect' => $request->maxRedirects,
                'name' => $name,
                'follow_location' => $request->followLocation,
                'store_sent_header' => $request->storeSentHeaders
            );
        }

        return new SpicaFutureResponseHandler($this->_handle, $opts);
    }

    /**
     * Gets the number of requests in queue
     *
     * @return int
     */
    public function count()
    {
        return $this->_counter;
    }

    /**
     * Closes the session explicitly.
     */
    public function close()
    {
        $this->_isClosed = true;
        $this->_session = null;
        $this->_executor->close();
    }

    /**
     * Tests if session is closed?
     *
     * @return bool
     */
    public function isClosed()
    {
        return $this->_isClosed;
    }
}

/**
 * This response handler provides a blocking getResponse() method, which will return if the response header is received.
 *
 * @category   spica
 * @package    core
 * @subpackage net
 * @author     Pham Cong Dinh <pcdinh at phpvietnam dot net>
 * @since      Version 0.3
 * @since      April 10, 2010
 * @copyright  Pham Cong Dinh (http://www.phpvietnam.net)
 * @license    http://www.gnu.org/licenses/lgpl-3.0.txt
 * @version    $Id: AsyncHttpClient.php 1869 2011-01-07 18:55:25Z pcdinh $
 */
class SpicaFutureResponseHandler
{
    /**
     * Master session handle.
     *
     * @var resource
     */
    private $_master;

    /**
     * Individual processed transfer handles.
     *
     * @var array
     */
    private $_transfers;

    /**
     * Individual transfer options.
     * 
     * @var array
     */
    private $_opts;

    /**
     * Transfer callbacks (an array in format: request name => SpicaAsyncClientCallback).
     *
     * @var array
     */
    private $_callbacks;

    /**
     * Constructs an object of <code>SpicaFutureResponseHandler</code>.
     *
     * @param resource $master Multi stack Curl resource handle
     * @param array $opts Options for eacg transfer handle
     * @param array $callbacks Optional callbacks for each transfer handle
     */
    public function __construct($master, $opts, $callbacks = array())
    {
        $this->_master = $master;
        $this->_opts = $opts;
        $this->_callbacks = $callbacks;
    }

    /**
     * Waits untils response header is received. This is a blocking call
     *
     * @throws Exception if there is any error with the whole multi stack
     * @return array An array of SpicaHttpResponse objects
     */
    public function getResponses()
    {
        curl_multi_select($this->_master);
        $responses = array();
        $redirectCount = 0;
        // Blocking operation
        do
        {
            $status = curl_multi_exec($this->_master, $active);
            $transfer = curl_multi_info_read($this->_master);

            if (false !== $transfer)
            {
                // Curl automatically decodes chunked-messages
                $resp = new SpicaHttpResponse($transfer['handle']);
                $httpCode = curl_getinfo($transfer['handle'], CURLINFO_HTTP_CODE);
                $body = curl_multi_getcontent($transfer['handle']);
                $options = $this->_opts[(string) $transfer['handle']];

                if (true === (bool) $options['store_sent_header'])
                {
                    $resp->setHttpHeaders(substr($body, 0, curl_getinfo($transfer['handle'], CURLINFO_HEADER_SIZE)));
                    $body = substr($body, curl_getinfo($transfer['handle'], CURLINFO_HEADER_SIZE));
                }

                // if there is a need to make another request in case of there is a 301 or 302 (redirect response)
                $newReq = false;

                if ($httpCode == 301 || $httpCode == 302)
                {                    
                    if (true === (bool) $options['store_sent_header'])
                    {
                        $matches = array();
                        preg_match('/Location:(.*?)\n/', $resp->getHttpHeaders(), $matches);
                        $url = parse_url(trim(array_pop($matches)));

                        if (true === is_array($url))
                        {
                            $lastUrl = parse_url(curl_getinfo($transfer['handle'], CURLINFO_EFFECTIVE_URL));
                            
                            if (!isset($url['scheme']))
                            {
                                $url['scheme'] = $lastUrl['scheme'];
                            }
                            
                            if (!isset($url['host']))
                            {
                                $url['host'] = $lastUrl['host'];
                            }

                            if (!isset($url['path']))
                            {
                                $url['path'] = $lastUrl['path'];
                            }

                            $newUrl = $url['scheme'] . '://' . $url['host'] . $url['path'] . (isset($url['query']) ? '?' . $url['query'] : '');
                            curl_multi_remove_handle($this->_master, $transfer['handle']);
                            curl_setopt($transfer['handle'], CURLOPT_URL, $newUrl);                            
                            curl_multi_add_handle($this->_master, $transfer['handle']);
                            curl_multi_exec($this->_master, $active);
                            $newReq = true;
                        }
                    }
                }

                if (false === $newReq)
                {
                    $resp->setBody($body);
                    $responses[] = $resp;
                    curl_multi_remove_handle($this->_master, $transfer['handle']);
                    $this->_transfers[] = $transfer['handle'];
                }
            }
            usleep(25000); // save CPU cycles
        } while ($status === CURLM_CALL_MULTI_PERFORM || $active);

        // Error happens, errors regarding the whole multi stack, not individual transfers
        if ($status != CURLM_OK)
        {
            throw new Exception(curl_error($this->_master), curl_errno($this->_master));
        }

        return $responses;
    }

    /**
     * Gets Curl transfers.
     *
     * @return array
     */
    public function getTransfers()
    {
        return $this->_transfers;
    }

    /**
     * Closes master Curl handle.
     */
    public function close()
    {
        if (null !== $this->_master)
        {
            curl_multi_close($this->_master);
        }

        if (!empty($this->_transfers))
        {
            foreach ($this->_transfers as $handle)
            {
                if (null !== $handle)
                {
                    curl_close($handle);
                }
            }
        }
    }
}

?>